package cn.amossun.starter.mybatis.data.security.rule;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.amossun.starter.common.crypto.rule.EncryptRule;
import cn.amossun.starter.mybatis.data.security.config.MybatisPlusDataSecurityConfig;
import cn.amossun.starter.mybatis.data.security.enums.SqlDmlTypeEnum;
import cn.amossun.starter.mybatis.data.security.runner.OrmConfigCacheRunner;
import cn.amossun.starter.mybatis.data.security.enums.ConfigTypeEnum;
import cn.amossun.starter.mybatis.data.security.properties.*;
import cn.amossun.starter.redis.cache.exception.RedisCacheConfigException;
import lombok.extern.slf4j.Slf4j;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 加密规则
 */
@Slf4j
public class MybatisEncryptRule extends EncryptRule {

    public MybatisEncryptRule(MybatisPlusDataSecurityConfig mybatisConfig, OrmConfigCacheRunner ormConfigCacheRunner) throws Exception {
        super(mybatisConfig.getMybatisPlus());
        this.mybatisConfig = mybatisConfig;
        this.ormConfigCacheRunner = ormConfigCacheRunner;
        this.ormConfigMap = buildMapByTableName(getOrmConfigs());
    }

    private final MybatisPlusDataSecurityConfig mybatisConfig;

    private final OrmConfigCacheRunner ormConfigCacheRunner;

    /**
     * 加解密ORM配置集合
     */
    private Map<String, ResultMapConfiguration> ormConfigMap;

    public boolean updateOrmConfigMap() throws Exception {
        this.ormConfigMap = buildMapByTableName(getOrmConfigs());
        return true;
    }

    public Map<String, ResultMapConfiguration> getOrmConfigMap() throws Exception {
        updateOrmConfigMap();
        return this.ormConfigMap;
    }

    protected List<ResultMapConfiguration> getOrmConfigs() throws ExecutionException, RedisCacheConfigException {
        if(mybatisConfig.getMybatisPlus() == null) {
            return null;
        }
        if(mybatisConfig.getMybatisPlus().getConfigType() == ConfigTypeEnum.YML) {
            return mybatisConfig.getMybatisPlus().getOrmConfigs();
        }
        if(mybatisConfig.getMybatisPlus().getConfigType() == ConfigTypeEnum.REDIS) {
            Object object = ormConfigCacheRunner.getCacheValue();
            if(object != null && object instanceof Optional && ((Optional<Object>)object).isPresent()) {
                return JSONUtil.toList(JSONUtil.parseArray(((Optional<Object>)object).get()), ResultMapConfiguration.class);
            }
            return null;
        }
        return null;
    }

    protected Map<String, ResultMapConfiguration> buildMapByTableName(List<ResultMapConfiguration> ormConfigs) {
        if(CollectionUtil.isEmpty(ormConfigs)) {
            return MapUtil.newHashMap(0);
        }
        if(log.isDebugEnabled()) {
            log.debug("ormConfigs info-> {}", JSONUtil.toJsonStr(ormConfigs));
        }
        Map<String, ResultMapConfiguration> resultMapConfigurationMap = new HashMap<>(ormConfigs.size());
        ormConfigs.stream().forEach(source->{
            Map<String, ResultConfiguration> propertyMap = source.getResults().stream().collect(Collectors.toMap(ResultConfiguration::getColumn, Function.identity(), (exiting, replacement) -> exiting));

            ResultMapConfiguration resultMapConfiguration = new ResultMapConfiguration();
            resultMapConfiguration.setTableName(source.getTableName());
            resultMapConfiguration.setPropertyMap(propertyMap);
            resultMapConfigurationMap.put(source.getTableName(), resultMapConfiguration);
        });
        return resultMapConfigurationMap;
    }

    public Optional<ResultConfiguration> getChildRuleConfiguration(String tableName, String columnName) throws Exception {

        if (!getOrmConfigMap().containsKey(tableName)) {
            return Optional.empty();
        }

        final ResultMapConfiguration resultMapConfiguration = getOrmConfigMap().get(tableName);

        final Map<String, ResultConfiguration> propertyMap = resultMapConfiguration.getPropertyMap();

        if (!propertyMap.containsKey(columnName.toLowerCase())) {
            return Optional.empty();
        }

        return Optional.of(propertyMap.get(columnName.toLowerCase()));
    }

    public boolean containsColumn(String tableName, String columnName) throws Exception {
        return getChildRuleConfiguration(tableName, columnName).isPresent();
    }

    public Object getEncryptedValue(ResultConfiguration configuration, Object originalValue) throws Exception {
        return getEncryptValue(configuration, originalValue);
    }

    public Object getEncryptedValue(String tableName, String columnName, Object originalValue, SqlDmlTypeEnum sqlDmlTypeEnum) throws Exception {
        Optional<ResultConfiguration> resultConfigurationOptional = getChildRuleConfiguration(tableName, columnName);
        if (!resultConfigurationOptional.isPresent()) {
            return originalValue;
        }
        ResultConfiguration configuration = resultConfigurationOptional.get();
        if (configuration.isEncrypt() || (sqlDmlTypeEnum != null && sqlDmlTypeEnum == SqlDmlTypeEnum.INSERT)) {
            return getEncryptValue(configuration, originalValue);
        }
        return originalValue;
    }

    private Object getEncryptValue(ResultConfiguration configuration, Object originalValue) throws BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, UnsupportedEncodingException {
        if (!(originalValue instanceof String) && !(originalValue instanceof Number)) {
            return originalValue;
        }
        if(null == originalValue) {
            return originalValue;
        }
        if(configuration != null && StrUtil.isNotEmpty(configuration.getEncryptDelimiter())) {
            List<String> originalValueList = Arrays.asList((String.valueOf(originalValue)).split(configuration.getEncryptDelimiter()));
            List<String> encryptedValueList = new ArrayList<>(originalValueList.size());
            for (String sourceStr : originalValueList) {
                encryptedValueList.add(encodeEncryptValue(sourceStr));
            }
            return StrUtil.join(configuration.getEncryptDelimiter(), encryptedValueList);
        }
        return encodeEncryptValue(String.valueOf(originalValue));
    }

}
